Skip to content

feat(appkit): migrate analytics to the modular @databricks/sdk-* - #562

Open
MarioCadenas wants to merge 10 commits into
mainfrom
analytics-migration-sdk
Open

MarioCadenas wants to merge 10 commits into
mainfrom
analytics-migration-sdk

Conversation

@MarioCadenas

@MarioCadenas MarioCadenas commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

What

Migrates the analytics stack (SQLWarehouseConnector + type-generator) off the legacy monolithic @databricks/sdk-experimental onto the new modular per-service @databricks/sdk-* SDK (v0.46.0, ESM-only), behind the existing packages/shared/src/workspace-client/ facade seam.

Analytics doesn't touch the SDK directly — it delegates to SQLWarehouseConnector, which uses two services: warehouses and statementExecution. Both move together in this increment; every other service (files/genie/jobs/serving/currentUser/apiClient) still routes through the legacy client behind the same facade (the seam's designed incremental state).

How

  • New boundary module packages/shared/src/workspace-client/modular.ts — the sole importer of @databricks/sdk-* (oxlint no-restricted-imports boundary), mirroring legacy.ts. Builds per-service WarehousesClient / StatementExecutionClient; maps wrapper options → ClientOptions (host scheme-normalization, credential resolution, and a httpClient transport that stamps the User-Agent).
  • Connector + type-generator rewritten to the modular API: method renames (getStatementgetStatementResult, getStatementResultChunkNgetResultData), camelCase response model, CallOptions { signal } for cancellation.
  • pnpm patch (patches/@databricks__sdk-statementexecution@0.46.0.patch) restores the undocumented Reyden attachment response field that the SDK's generated unmarshal transform would otherwise strip. Version-pinned — re-apply on any SDK bump; an upstream fix is preferred long-term.

Authentication (the foundation for migrating the remaining services)

Auth resolution is centralized in one service-agnostic function, mapToClientOptions(opts) in modular.ts, which produces the ClientOptions { host, credentials, httpClient } that every modular client constructor accepts. Warehouses and statementExecution build through it today; each remaining service will build through the same function, so the rest of the migration needs no per-service auth work (the credentials are generic bearer TokenCredentials, not service-scoped).

Resolution order — restores the legacy SDK's behavior for the modes AppKit uses:

  • explicit token (asUser OBO) → PAT, guarded by token !== undefined so an empty/invalid OBO token fails loudly instead of silently falling through to the service principal (privilege-escalation guard);
  • profile → profile file;
  • otherwise the service principal from the environmentDATABRICKS_CLIENT_ID + DATABRICKS_CLIENT_SECRET → OAuth M2M (what the Databricks Apps runtime injects), else DATABRICKS_TOKEN → PAT, else the SDK's profile-based default chain.

Why resolve the SP explicitly rather than lean on the SDK's default chain: the default chain does read the DATABRICKS_* env vars, but its M2M strategy performs OAuth token-endpoint discovery against the raw DATABRICKS_HOST, which the Apps runtime injects scheme-less (e.g. e2-dogfood.staging.cloud.databricks.com). Discovery then throws Invalid URL and every deployed request fails as "Warehouse readiness check failed". Resolving the SP here with the scheme-normalized host avoids that (it's the credential-side facet of the host-normalization fix below). Verified end-to-end on a deployed app.

Not a full 1-to-1 with legacy's auth surface: the modular SDK 0.46.0 supports only PAT, OAuth M2M, and OAuth U2M — there are no factories for Azure (MSI/CLI/SP), GCP, or the instance-metadata service that legacy unified-auth covered. This is behaviorally equivalent for AppKit's real environments (Databricks Apps = SP via env; local dev = profile / PAT / env) but a reduced surface for those exotic modes; noted for release notes.

Per-service OBO scopes: the auth code is uniform, but each OBO-consuming service needs its matching user_api_scopes declared in the app's databricks.ymlsql (warehouse/statement), files.files, dashboards.genie, serving.serving-endpoints. That's app configuration, not auth code.

Runtime bugs found + fixed (caught against live warehouses / a deployed app, not unit tests)

  1. Host scheme (requests) — the modular SDK passes host straight to fetch; a bare DATABRICKS_HOSTInvalid URL. Fixed via normalizeHost on the request host.
  2. User-AgentsetProduct("@databricks/appkit") throws (the @// aren't token chars) and client-info is process-global. AppKit dashboards filter traffic on the literal @databricks/appkit UA, so we set it on a per-client httpClient transport wrapper instead — preserving the exact string. (A pnpm patch of the validator wouldn't reach deployed apps, which is where the dashboard traffic originates.)
  3. ApiError.code — the modular ApiError carries the Databricks error code on .code, not .errorCode; the connector now reads either, so the arrow disposition/format capability-rejection fallback (INLINE↔EXTERNAL_LINKS) fires again.
  4. BigInt serialization — the modular SDK types row/byte counts as bigint; Reyden's cached INLINE+ARROW_STREAM result then broke JSON.stringify. Counts are coerced back to number at the connector boundary.
  5. Deployed service-principal auth — the SDK's M2M strategy builds credentials from the raw scheme-less DATABRICKS_HOST the Apps runtime injects, so OAuth token discovery failed (Invalid URL) and every deployed query died as "Warehouse readiness check failed". Fixed by resolving the SP with a scheme-normalized host (see Authentication).

Testing

  • Full appkit + shared suites green, including new regression tests for the .code read, bigint normalization, the env-based credential resolution (M2M-from-env, PAT-from-env, OBO-token-wins, M2M-needs-host), and the @databricks/appkit User-Agent stamping.
  • pnpm -r typecheck, lint, format clean; pnpm install --frozen-lockfile green (appkit importer synced in pnpm-lock.yaml).
  • Verified live against standard + Reyden serverless warehouses: JSON + arrow (INLINE attachment and EXTERNAL_LINKS), OBO (.obo.sql), warehouse auto-start, metric views.
  • Verified end-to-end on a deployed Databricks App: an SP-default query and an OBO (asUser, user_api_scopes: [sql]) query both execute against the SQL warehouse.

Notes

  • Metric views run entirely on the migrated services (they compile to SQL); no separate migration needed.
  • Regenerated the workspace-client API docs to match the new types (included).

This pull request and its description were written by Isaac.

@MarioCadenas
MarioCadenas force-pushed the analytics-migration-sdk branch from 9e1a7d5 to 2931182 Compare September 10, 2026 14:43
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle size report

Compared against bundle-size-baseline.json (main).

@databricks/appkit

npm tarball (packed): 1.2 MB (+43 KB) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 1.2 MB (+36 KB) 428 KB (+15 KB)
Type declarations 445 KB (+17 KB) 161 KB (+7.6 KB)
Source maps 2.4 MB (+73 KB) 801 KB (+28 KB)
Other 11 KB 3.7 KB
Total 4.0 MB (+127 KB) 1.4 MB (+51 KB)
Per-entry composition (own code — deps external (as shipped))
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
. 96 KB (+677 B) 2.5 KB 99 KB (+677 B) external 315 KB (+1.8 KB)
./beta 93 KB (+622 B) 456 B (-1 B) 94 KB (+621 B) external 281 KB (+1.5 KB)
./testing 39 KB (+22 KB) 30 KB (+30 KB) 69 KB (+52 KB) external 202 KB (+150 KB)
./tsdown 520 B 0 B 520 B external 813 B
./type-generator 23 KB (+442 B) 0 B 23 KB (+442 B) external 67 KB (+1.3 KB)

Chunks:

Entry Chunk Load Size (gz)
. index.js initial 92 KB
. utils.js initial 4.0 KB
. remote-tunnel-manager.js lazy 2.5 KB
./beta beta.js initial 76 KB
./beta stream-manager.js initial 5.8 KB
./beta wide-event-emitter.js initial 3.2 KB
./beta databricks.js initial 3.2 KB
./beta configuration.js initial 2.3 KB
./beta service-context.js initial 1.3 KB
./beta modular.js initial 960 B
./beta client-options.js initial 220 B
./beta supervisor-api.js lazy 191 B
./beta databricks.js lazy 142 B
./beta index.js lazy 123 B
./testing manifest.js initial 26 KB
./testing index.js initial 9.9 KB
./testing wide-event-emitter.js initial 2.9 KB
./testing index.js lazy 26 KB
./testing remote-tunnel-manager.js lazy 2.5 KB
./testing utils.js lazy 1.2 KB
./tsdown index.js initial 520 B
./type-generator index.js initial 23 KB

@databricks/appkit-ui

npm tarball (packed): 350 KB (+83 B) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 395 KB 132 KB
Type declarations 229 KB (+155 B) 84 KB (+114 B)
Source maps 766 KB 253 KB
CSS 16 KB 3.2 KB
Total 1.4 MB (+155 B) 473 KB (+114 B)
Per-entry composition (consumer bundle — deps bundled, peerDeps external)
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
./js 5.3 KB 49 KB 55 KB 208 KB 14 KB
./js/beta 20 B 0 B 20 B 0 B 0 B
./react 432 KB 49 KB 481 KB 1.3 MB 177 KB
./react/beta 1.0 KB 0 B 1.0 KB 0 B 1.9 KB

Chunks:

Entry Chunk Load Size (gz)
./js index.js initial 5.2 KB
./js chunk initial 120 B
./js apache-arrow lazy 49 KB
./js/beta beta.js initial 20 B
./react index.js initial 430 KB
./react tslib initial 2.1 KB
./react apache-arrow lazy 49 KB
./react/beta beta.js initial 1.0 KB

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

🤖 AppKit PR bot

🔬 Run evals

Start an eval for this PR from the evals-monitor app: Go to Evals Monitor →

📦 Try this PR's app template

Scaffolds a new app from this PR's SDK build. Run it in any folder (requires the GitHub CLI — gh auth login — and the Databricks CLI):

gh run download 35002699754 -R databricks/appkit -n appkit-template-0.75.1-pr.f1385e9-analytics-migration-sdk-562 -D appkit-pr-562 \
  && unzip -o "appkit-pr-562/appkit-template-0.75.1-pr.f1385e9-analytics-migration-sdk-562.zip" -d "appkit-pr-562" \
  && databricks apps init --template "appkit-pr-562"

The template pins @databricks/appkit and @databricks/appkit-ui to tarballs built from this branch, so the scaffolded app runs against this PR's code.

@MarioCadenas
MarioCadenas force-pushed the analytics-migration-sdk branch from 613983c to 53566af Compare September 14, 2026 09:47
@MarioCadenas
MarioCadenas marked this pull request as ready for review September 14, 2026 10:50
@MarioCadenas
MarioCadenas requested a review from a team as a code owner September 14, 2026 10:50
@MarioCadenas
MarioCadenas force-pushed the analytics-migration-sdk branch from 5a99f30 to 27a72cc Compare September 14, 2026 13:24
MarioCadenas and others added 8 commits September 15, 2026 16:41
Migrate the analytics stack (SQLWarehouseConnector + type-generator) off
the legacy monolithic @databricks/sdk-experimental onto the new modular
per-service @databricks/sdk-* SDK (v0.46.0, ESM-only), behind the existing
workspace-client facade seam. The two services analytics depends on —
warehouses and statementExecution — move together; every other service
still routes through the legacy client (mixed state by design).

- New packages/shared/src/workspace-client/modular.ts is the sole importer
  of @databricks/sdk-* (oxlint no-restricted-imports boundary), mirroring
  legacy.ts. Builds per-service WarehousesClient / StatementExecutionClient;
  maps wrapper options -> ClientOptions (host scheme-normalization, PAT
  empty-token guard, profile); stamps process-global client-info
  (sanitized, best-effort).
- Connector + type-generator rewritten to the modular API: method renames
  (getStatement -> getStatementResult, getStatementResultChunkN ->
  getResultData), camelCase response model, CallOptions { signal }.
- statementExecution relies on a pinned pnpm patch that restores the
  undocumented Reyden `attachment` field the SDK's unmarshal transform
  would otherwise strip.
- Coerce the SDK's bigint row/byte counts back to number at the connector
  boundary so INLINE + ARROW_STREAM results stay JSON-serializable
  (cache / SSE frames).
- Read the modular ApiError's `.code` (not only the legacy `.errorCode`) so
  the arrow disposition/format capability-rejection fallback still fires.

Verified against live warehouses (standard + Reyden serverless): JSON and
arrow (INLINE attachment + EXTERNAL_LINKS), OBO, warehouse auto-start, and
metric views. Full appkit + shared suite green (3877 tests).

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
…onse mappers

Completes the analytics SDK migration: DatabricksStatementExecutionResponse is
now camelCase (aligned with the modular SDK's StatementResponse), so the
translation shims the migration introduced are no longer needed.

- Delete `toDescribeResponse` — `describeAdaptive` narrows the SDK response onto
  the domain type directly (one cast: the SDK types DESCRIBE cells as
  JsonValue[][], but for a DESCRIBE they are always string/null).
- Delete the three duplicated `asSdkResponse` test helpers; fixtures are now
  authored in the camelCase domain shape and feed both the SDK-mock and the
  snake-free parsers directly.
- `error_code` stays snake ONLY where it reads the raw Databricks error wire
  shape (errors.ts auth classification, `{"error_code":...}` JSON bodies) — a
  network payload, not our type.

Internal type only (not exported, not in API docs) — no breaking change.
Net -117 LOC. Verified: typecheck, 4355 tests, build (attw + publint), lint, format.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Rebasing the analytics SDK migration onto #540 (createTestApp + never-crash
mock client) surfaced that #540's mock hardcodes the LEGACY warehouse method
names. Point them at the modular API so warehouse-readiness calls resolve:
- mock-workspace-client.ts DEFAULT_RESPONSES: warehouses.get/start ->
  getWarehouse/startWarehouse.
- mock-workspace-client.test.ts: never-crash table + convergence asserts.

(fixtures.ts + analytics.integration.test.ts adaptations landed inside the
migration commit during rebase conflict resolution.)

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
appkit bundles `shared` inline, whose code imports the modular @databricks/sdk-*
packages, but only @databricks/sdk-experimental was declared. A published
`@databricks/appkit` install (npm, incl. the Databricks Apps runtime) would
therefore fail at runtime with "Cannot find module
@databricks/sdk-statementexecution" on the analytics path. Declare
sdk-auth/core/options/warehouses/statementexecution at 0.46.0 (mirroring shared)
and exempt them from knip's unused check like sdk-experimental (they are imported
only by the inlined `shared` workspace, which knip does not analyze).

Known follow-up: the pnpm patch restoring Reyden's stripped `attachment` field is
a workspace-only mechanism, does not reach consumers, and cannot be bundled under
tsdown `unbundle` mode. Standard warehouses (dataArray + external_links) are
unaffected; Reyden INLINE+ARROW_STREAM needs the upstream SDK `attachment` fix.
See SDK_MIGRATION_GAPS.md.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
…r SDK client

The modular @databricks/sdk-* default credential chain resolves auth only
from a ~/.databrickscfg profile and reads no DATABRICKS_* env vars, unlike
the legacy sdk-experimental. The Databricks Apps runtime injects the app's
service-principal credentials via env vars only (no config file), so a
deployed app built a modular client with no credentials and every request
failed ("Warehouse readiness check failed").

Resolve the service principal from the environment in mapToClientOptions
when no explicit token/profile is given: M2M from DATABRICKS_CLIENT_ID +
DATABRICKS_CLIENT_SECRET (what Apps injects), else PAT from DATABRICKS_TOKEN,
else fall through to the profile default chain for local dev. The explicit
token path (asUser OBO) is unchanged and still guarded by token !== undefined
so an empty/invalid OBO token fails loudly instead of silently falling
through to the service principal.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
The 5 modular @databricks/sdk-* deps were added to packages/appkit/package.json
but the appkit importer in pnpm-lock.yaml was never regenerated, so CI's
`pnpm install --frozen-lockfile` failed with ERR_PNPM_OUTDATED_LOCKFILE.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
…ients

The modular SDK's client-info `setProduct` validates the product as a token
and rejects `@databricks/appkit` (the `@`/`/`), so the migrated clients sent a
sanitized `databricks-appkit` User-Agent. Databricks-side dashboards filter
AppKit's analytics/warehouse traffic on the literal `@databricks/appkit` UA, so
that silently dropped AppKit out of them.

Set the User-Agent on a per-client `ClientOptions.httpClient` transport wrapper
(`buildHttpClient`) that prepends `@databricks/appkit/<version>` (+ userAgentExtra)
to each request, replacing the process-global `setProduct` sanitization/latch.
This keeps the exact legacy string and ships inside appkit's bundled dist, so it
reaches deployed apps — a pnpm patch of the validator would not (pnpm patches
apply only at workspace install, not to the npm-installed tarball).

Also corrects the service-principal auth comments: the SDK's default chain does
read DATABRICKS_* env; the deployed failure was its M2M strategy using the raw
scheme-less DATABRICKS_HOST for OAuth discovery, which the normalized-host
resolution already works around.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas
MarioCadenas force-pushed the analytics-migration-sdk branch from b8820bd to 647b566 Compare September 15, 2026 14:46
MarioCadenas and others added 2 commits September 15, 2026 18:35
The Reyden `attachment` fix is a pnpm patch, which applies only at this
monorepo's install and does NOT travel through a normal `npm install`. A
deployed app resolved the unpatched registry copy and lost `attachment` on
the INLINE+ARROW_STREAM path (masked by the EXTERNAL_LINKS fallback where
supported, broken on INLINE-only Reyden warehouses).

Ship the patched copy inside appkit's tarball via npm `bundledDependencies`:
`dist-appkit.ts` copies the pnpm-patched package into the tarball's
node_modules and declares it bundled, so the consumer (including the
Databricks Apps runtime's npm) resolves appkit's imports to the patched copy.
Verified: a consumer install resolves the nested patched copy with no
unpatched top-level copy. Extensible via the BUNDLED_PATCHED_PACKAGES list.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
dist-appkit.ts builds both the appkit and appkit-ui tarballs. The
bundledDependencies step hard-failed on appkit-ui, which does not depend on
@databricks/sdk-statementexecution. Gate bundling on the tarball package's own
declared dependencies (captured before the CLI-dependency merge), so appkit-ui
bundles nothing and appkit still bundles the patched statementexecution.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant